/*!
 *
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 *
 * Copyright (c) 2002-2018 Hitachi Vantara. All rights reserved.
 *
 */

package org.pentaho.platform.web.http.api.resources;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.platform.api.engine.IContentGenerator;
import org.pentaho.platform.api.engine.IOutputHandler;
import org.pentaho.platform.api.engine.IParameterProvider;
import org.pentaho.platform.api.engine.IPentahoUrlFactory;
import org.pentaho.platform.api.engine.IPluginManager;
import org.pentaho.platform.api.repository2.unified.RepositoryFile;
import org.pentaho.platform.engine.core.solution.SimpleParameterProvider;
import org.pentaho.platform.engine.core.system.PentahoRequestContextHolder;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.core.system.PentahoSystem;
import org.pentaho.platform.repository.RepositoryFilenameUtils;
import org.pentaho.platform.util.messages.LocaleHelper;
import org.pentaho.platform.util.web.MimeHelper;
import org.pentaho.platform.util.web.SimpleUrlFactory;
import org.pentaho.platform.web.http.HttpOutputHandler;
import org.pentaho.platform.web.http.api.resources.GeneratorStreamingOutputProvider.MimeTypeCallback;
import org.pentaho.platform.web.http.request.HttpRequestParameterProvider;
import org.pentaho.platform.web.http.session.HttpSessionParameterProvider;
import org.pentaho.platform.web.servlet.HttpMimeTypeListener;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;

import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GeneratorStreamingOutput {

  private static final Log logger = LogFactory.getLog( GeneratorStreamingOutput.class );

  private static final Log mimeTypeLogger = LogFactory.getLog( "MIME_TYPE" ); //$NON-NLS-1$

  protected IContentGenerator contentGenerator;

  protected String contentGeneratorID;

  protected RepositoryFile file;

  protected String command;

  protected HttpServletRequest httpServletRequest;

  protected HttpServletResponse httpServletResponse;

  protected IPluginManager pluginMgr;

  protected String fileType;

  protected String mimeType;

  protected List<MediaType> acceptableMediaTypes = new ArrayList<MediaType>();

  protected ContentGeneratorDescriptor contentGeneratorDescriptor;

  private static final boolean MIMETYPE_MUTABLE = true;

  /**
   * Invokes a content generator to produce some content either in the context of a repository file, or in the form of a
   * direct service call (no repository file in view).
   * 
   * @param contentGenerator
   *          the content generator to invoke
   * @param ContentGeneratorDescriptor
   *          a descriptor detailing info about the content generator
   * @param request
   *          the HTTP request
   * @param response
   *          the HTTP response
   * @param producesMimeType
   *          the requested return type of the output (can be null if none is preferred)
   * @param file
   *          the repository file being rendered (can be null if a repository file does not apply)
   * @param command
   *          the trailing part of the URL path of the request, typically used as a command sequence (can be null)
   */
  public GeneratorStreamingOutput( IContentGenerator contentGenerator, ContentGeneratorDescriptor desc,
      HttpServletRequest request, HttpServletResponse response, List<MediaType> acceptableMediaTypes,
      RepositoryFile file, String command ) {
    if ( contentGenerator == null ) {
      throw new IllegalArgumentException( "contentGenerator cannot be null" );
    }
    this.contentGenerator = contentGenerator;
    this.contentGeneratorDescriptor = desc;
    this.command = command;
    this.contentGeneratorID = desc.getContentGeneratorId();
    this.fileType = desc.getServicingFileType();
    this.file = file;
    mimeTrace( "Request is requiring content generator to return content of type [{0}]", acceptableMediaTypes
        .toString() );
    if ( acceptableMediaTypes != null ) {
      this.acceptableMediaTypes = acceptableMediaTypes;
    }
    this.httpServletRequest = request;
    this.httpServletResponse = response;
    pluginMgr = PentahoSystem.get( IPluginManager.class );

    // if command seems like a file path, then use the file extension to determine response mime type, otherwise
    // leave it up to the content generator
    if ( command != null && command.contains( "." ) ) { //$NON-NLS-1$
      String tmpMimeType = MimeHelper.getMimeTypeFromFileName( command );
      if ( tmpMimeType != null ) {
        mimeType = tmpMimeType;
        mimeTrace( "Setting response mime type to [{0}] (based on extension of resource [{1}])", mimeType, command ); //$NON-NLS-1$
      }
    }
  }

  private void mimeTrace( String msg, Object... args ) {
    if ( mimeTypeLogger.isDebugEnabled() ) {
      String prologue =
          MessageFormat.format(
            "<< {3} [MIME TRACE] Content generator id [{0}] for file type [{1}] and resource [{2}]>>: ",
            contentGeneratorID, fileType, command, this );
      mimeTypeLogger.debug( MessageFormat.format( prologue + msg, args ) );
    }
  }

  public void write( OutputStream output, MimeTypeCallback callback ) throws IOException {
    if ( file != null ) {
      fileType = RepositoryFilenameUtils.getExtension( file.getName() );
    }

    try {
      if ( !MIMETYPE_MUTABLE && getMimeType() != null ) {
        // the mime type has been predetermined
        mimeTrace(
            "Return mime type has been pretermined based on the resource addressed in the URI [{0}]."
              + " Forcing content generator to return content of type [{1}]",
            command, getMimeType() );
        callback.setMimeType( getMimeType() );
      }

      generateContent( output, callback );
    } catch ( Exception e ) {
      // logging here because it's the last place we can log to file before
      // the error is streamed back in the http 500 response
      String msg =
           MessageFormat.format( "Error generating content from content generator with id [{0}]", contentGeneratorID );
      logger.error( msg, e );
      throw new IOException( msg, e );
    } finally {
      PentahoSystem.systemExitPoint();
    }
  }

  protected void generateContent( OutputStream outputStream, final MimeTypeCallback callback ) throws Exception {
    try {
      httpServletResponse.setCharacterEncoding( LocaleHelper.getSystemEncoding() );
    } catch ( Throwable t ) {
      logger.warn( "could not set encoding, servlet-api is likely too old.  are we in a unit test?" ); //$NON-NLS-1$
    }

    IOutputHandler outputHandler = new HttpOutputHandler( httpServletResponse, outputStream, true );
    outputHandler.setMimeTypeListener( new HttpMimeTypeListener( httpServletRequest, httpServletResponse ) {
      /*
       * This content generator is setting the mimeType
       */
      @Override
      public void setMimeType( String mimeType ) {
        try {
          if ( !MIMETYPE_MUTABLE && GeneratorStreamingOutput.this.getMimeType() != null ) {
            mimeTrace(
                "Content generator is trying to set response mime type to [{0}], but mime type [{1}] has already been imposed. Content generator request to change mime type will be ignored.", //$NON-NLS-1$
                mimeType, GeneratorStreamingOutput.this.getMimeType() );
            return;
          } else {
            mimeTrace( "Content generator is setting response mime type to [{0}]", mimeType ); //$NON-NLS-1$
            if ( callback != null ) {
              callback.setMimeType( mimeType );
            }
            GeneratorStreamingOutput.this.setMimeType( mimeType );
          }
        } catch ( Throwable th ) {
          mimeTrace( "Failed to set mime type: {0}", th.getMessage() );
          logger.error( MessageFormat.format( "Failed to set mime type: {0}", th.getMessage() ) ); //$NON-NLS-1$
        }
        super.setMimeType( mimeType );
      }
    } );

    Map<String, IParameterProvider> parameterProviders = new HashMap<String, IParameterProvider>();
    parameterProviders.put( IParameterProvider.SCOPE_REQUEST, createRequestParamProvider() );
    parameterProviders.put( IParameterProvider.SCOPE_SESSION, createSessionParameterProvider() );
    parameterProviders.put( "headers", createHeaderParamProvider() ); //$NON-NLS-1$
    parameterProviders.put( "path", createPathParamProvider() ); //$NON-NLS-1$

    String pluginId = contentGeneratorDescriptor.getPluginId();

    IPentahoUrlFactory urlFactory =
        new SimpleUrlFactory( PentahoRequestContextHolder.getRequestContext().getContextPath()
            + "api/repos/" + pluginId + "/" + contentGeneratorID + "?" ); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

    // set the classloader of the current thread to the class loader of
    // the plugin so that it can load its libraries
    // Note: we cannot ask the contentGenerator class for it's classloader,
    // since the cg may
    // actually be a proxy object loaded by main the WebAppClassloader
    ClassLoader origContextClassloader = Thread.currentThread().getContextClassLoader();
    Thread.currentThread().setContextClassLoader( pluginMgr.getClassLoader( pluginId ) );
    try {
      contentGenerator.setOutputHandler( outputHandler );
      contentGenerator.setMessagesList( new ArrayList<String>() );
      contentGenerator.setParameterProviders( parameterProviders );
      contentGenerator.setSession( PentahoSessionHolder.getSession() );
      if ( urlFactory != null ) {
        contentGenerator.setUrlFactory( urlFactory );
      }
      contentGenerator.createContent();
    } finally {
      Thread.currentThread().setContextClassLoader( origContextClassloader );
    }
  }

  protected IParameterProvider createRequestParamProvider() {
    return new HttpRequestParameterProvider( httpServletRequest );
  }

  protected IParameterProvider createSessionParameterProvider() {
    return new HttpSessionParameterProvider( PentahoSessionHolder.getSession() );
  }

  protected IParameterProvider createHeaderParamProvider() {
    SimpleParameterProvider headerParams = new SimpleParameterProvider();
    Enumeration<?> names = httpServletRequest.getHeaderNames();
    while ( names.hasMoreElements() ) {
      String name = (String) names.nextElement();
      String value = httpServletRequest.getHeader( name );
      headerParams.setParameter( name, value );
    }
    return headerParams;
  }

  protected IParameterProvider createPathParamProvider() throws IOException {
    SimpleParameterProvider pathParams = null;
    pathParams = new SimpleParameterProvider();
    pathParams.setParameter( "query", httpServletRequest.getQueryString() ); //$NON-NLS-1$

    List<String> mediaTypes = new ArrayList<String>( acceptableMediaTypes.size() );
    for ( MediaType type : acceptableMediaTypes ) {
      mediaTypes.add( type.toString() );
    }
    pathParams.setParameter( "acceptableMediaTypes", mediaTypes ); //$NON-NLS-1$
    if ( mediaTypes.size() > 0 ) {
      pathParams.setParameter( "contentType", acceptableMediaTypes.get( 0 ) ); //$NON-NLS-1$
    }
    pathParams.setParameter( "inputstream", httpServletRequest.getInputStream() ); //$NON-NLS-1$
    pathParams.setParameter( "httpresponse", httpServletResponse ); //$NON-NLS-1$
    pathParams.setParameter( "httprequest", httpServletRequest ); //$NON-NLS-1$
    pathParams.setParameter( "remoteaddr", httpServletRequest.getRemoteAddr() ); //$NON-NLS-1$
    if ( file != null ) {
      pathParams.setParameter( "path", URLEncoder.encode( file.getPath(), "UTF-8" ) ); //$NON-NLS-1$
      pathParams.setParameter( "file", file ); //$NON-NLS-1$
    }
    if ( command != null ) {
      // path beyond that which matched the GeneratorResource
      pathParams.setParameter( "cmd", command ); //$NON-NLS-1$
    }
    return pathParams;
  }

  public String getMimeType() {
    return mimeType;
  }

  public void setMimeType( String mimeType ) {
    this.mimeType = mimeType;
  }
}
